package biolabs.core

import biolabs.mcl.{AnalysisError, AnalysisSuccess}
import biolabs.mcl.ModelDefinition
import biolabs.mcl.syntax.MCLParsers
import biolabs.mcl.syntax.ModelDefinitionAST

import java.io.PrintWriter
import java.io.StringWriter

import scala.tools.nsc.{Settings}
import scala.tools.nsc.interpreter.{IMain, Results}
import scala.util.matching.Regex
import scala.util.parsing.combinator.Parsers

/**
 * Result of script interpretation.
 *
 * @param output contains the interpreter output
 * or a description of an error.
 * @param prev if the script resulted in multiple invocations
 * of underlying Scala interpreter (as is the case when the
 * script contains model definitions in MCL), points to a chain
 * of previous results.
 */
abstract class InterpreterResult(
		val output: String,
		val prev: Option[InterpreterResult]
) {
	/**
	 * Determines whether the interpretation was successful.
	 */
	val successful: Boolean
}

/**
 * Result of a successful interpreter run.
 *
 * @param value result value of the interpreted script, if any.
 */
case class InterpreterSuccess(
		val value: Option[Any],
		override val output: String,
		override val prev: Option[InterpreterResult]
) extends InterpreterResult(output, prev) {
	override val successful = true
}

/**
 * Common ancestor for unsuccessful results.
 */
abstract class InterpreterError(
		override val output: String,
		override val prev: Option[InterpreterResult]
) extends InterpreterResult(output, prev) {
	override val successful = false
}

/**
 * Error in the Scala interpreter.
 *
 * @param result original result from the Scala interpreter.
 */
case class ScalaError(
		val result: Results.Result,
		override val output: String,
		override val prev: Option[InterpreterResult]
) extends InterpreterError(output, prev)

/**
 * An error in the model definition in MCL.
 * This can be a syntactic or a semantic error.
 * @param output contains the description of the MCL problem.
 */
case class MCLError(
		override val output: String,
		override val prev: Option[InterpreterResult]
) extends InterpreterError(output, prev)


object ScriptInterpreter {

	/**
	 * Name of the variable holding the Application,
	 * available to all scripts in the global scope.
	 */
	private final val APP = "APP"

	/**
	 * Name of the variable serving as a box
	 * for result value of expression evaluation.
	 */
	private final val RESULT_BOX = "RESULT_BOX"

	/**
	 * Script that initializes the core environment.
	 */
	private final val INIT_SCRIPT: String = getContent(ScriptInterpreter.getClass().getResourceAsStream("env.init"))

	/**
	 * Name of the initialization script in user's home directory.
	 */
	private final val USER_SCRIPT = ".biolabs.init"

	private final val compilerPath = jarPathOfClass("scala.tools.nsc.Interpreter")
	private final val libraryPath = jarPathOfClass("scala.ScalaObject")

	/**
	 * An instance of this class will be bound in the interpreter
	 * and serve to store the result of expression evaluation.
	 */
	sealed class ResultBox {
		var value: Any = null
	}

	private def getContent(is: java.io.InputStream): String = {
		val r = new java.io.InputStreamReader(is, "UTF-8")
		val sw = new java.io.StringWriter()
		var c = r.read()
		while(c != -1) {
			sw.write(c)
			c = r.read()
		}
		r.close()
		return sw.toString()
	}

	private def getContent(file: String): String = {
		try {
			return getContent(new java.io.FileInputStream(file))
		} catch {
			case e: java.io.FileNotFoundException => return null
		}
	}

	private def getSettings(): Settings = {
		val settings = new Settings()
		val origBootclasspath = settings.bootclasspath.value
		settings.bootclasspath.value = List(origBootclasspath, compilerPath, libraryPath).mkString(java.io.File.pathSeparator)
		settings.classpath.value = System.getProperty("java.class.path")
		return settings
	}

	private def jarPathOfClass(className: String): String = {
		val resource = className.split('.').mkString("/", "/", ".class")
		val path = getClass.getResource(resource).getPath
		val indexOfFile = path.indexOf("file:")
		val indexOfSeparator = path.lastIndexOf('!')
		path.substring(indexOfFile+5, indexOfSeparator)
	}
}

/**
 * Interprets user scripts (Scala and Model Composition Language (MCL)).
 * Uses {@link scala.tools.nsc.interpreter.IMain} to interpret Scala code.
 *
 * @throws RuntimeException if the initialization of the interpreter fails.
 * This can happen due to an error in the {@code .biolabs.init} init script
 * in user's home directory.
 */
class ScriptInterpreter(app: Application) {

	private val MCL_PARSERS = new MCLParsers()

	/**
	 * Regular expression that matches the start of model definition.
	 * (?m) turns on multiline mode.
	 * ^ matches the beginning of a line.
	 * \s* matches any sequence of whitespace.
	 * \b matches word boundary.
	 */
	private final val MCL_START = new Regex("(?m)^\\s*(abstract\\s+)?model\\b")

	private val buffer = new StringWriter();
	private val out = new PrintWriter(buffer);
	private val interpreter = new IMain(ScriptInterpreter.getSettings(), out)
	private val resultBox = new ScriptInterpreter.ResultBox()

	interpreter.bind(ScriptInterpreter.APP, classOf[Application].getCanonicalName(), app)
	interpreter.bind(ScriptInterpreter.RESULT_BOX, classOf[ScriptInterpreter.ResultBox].getCanonicalName(), resultBox)
	interpret(ScriptInterpreter.INIT_SCRIPT) match {
		case res: InterpreterError => throw new RuntimeException("Error in initialization:\n" + res.output)
		case _ => // do nothing
	}

	// try user's init file
	{
		val userScriptPath = System.getProperty("user.home") +
				java.io.File.separator +
				ScriptInterpreter.USER_SCRIPT
		val userScript = ScriptInterpreter.getContent(userScriptPath)
		if(userScript != null)
			interpret(userScript) match {
				case res: InterpreterError => throw new RuntimeException("Error in user's init script " + userScriptPath + ":\n" + res.output)
				case _ => // do nothing
			}
	}

	private def interpretScala(script: String, previousResult: Option[InterpreterResult]): InterpreterResult = {
		// interpret the script
		val res = interpreter.interpret(script)

		// get the interpreter output and reset the buffer
		out.flush()
		val msg = buffer.toString()
		buffer.getBuffer().setLength(0)

		res match {
			case Results.Success =>
				interpreter.typeOfExpression(script) match {
					case Some(t) =>
						if(t.toString() == "Unit")
							InterpreterSuccess(None, msg, previousResult)
						else
							InterpreterSuccess(interpreter.valueOfTerm(interpreter.mostRecentVar), msg, previousResult)
					case None =>
						InterpreterSuccess(None, msg, previousResult)
				}
			case _ =>
				ScalaError(res, msg, previousResult)
		}
	}

	def evalExpr(expr: String): InterpreterResult = {
		interpreter.typeOfExpression(expr) match {
			case None => ScalaError(Results.Error, "Invalid expression: " + expr, None)
			case Some(t) =>
				if(t.toString() == "Unit") {
					ScalaError(Results.Error, "Expression of type Unit: " + expr, None)
				} else {
					interpretScala(ScriptInterpreter.RESULT_BOX + ".value = (" + expr + ")", None) match {
						case err: ScalaError => err
						case InterpreterSuccess(_, out, prev) => InterpreterSuccess(Some(resultBox.value), out, prev)
					}
				}
		}
	}

	def interpret(script: String): InterpreterResult = {
		var start = 0
		var m: Option[Regex.Match] = MCL_START.findFirstMatchIn(script)
		var prev: Option[InterpreterResult] = None
		while(m.isDefined) {

			// interpret the Scala script preceding the model definition
			val scalaScript = script.substring(start, start + m.get.start)
			if(scalaScript.trim() != "")
				interpretScala(scalaScript, prev) match {
					case res: InterpreterSuccess => prev = Some(res)
					case err: InterpreterError => return err
				}

			// parse the model definition
			start += m.get.start
			val pRes = MCL_PARSERS.parse(MCL_PARSERS.modelDefinition, script.substring(start));
			pRes match {
				case _: MCL_PARSERS.Success[_] =>
					// semantic analysis
					val model = ModelDefinition.analyze(pRes.get, app) match {
						case AnalysisSuccess(md) => md
						case AnalysisError(msg) => return MCLError(msg, prev)
					}

					// print the generated code
					System.out.println(model.code) // XXX

					// interpret the generated code
					interpretScala(model.code, prev) match {
						case res: InterpreterSuccess => prev = Some(res)
						case err: InterpreterError => return err
					}

					// continue from the end of the model definition
					start += pRes.next.offset
					m = MCL_START.findFirstMatchIn(script.substring(start))

				case fail: MCL_PARSERS.NoSuccess =>
					return MCLError(fail.msg, prev)
			}
		}

		// interpret the rest of the Scala script (if non-empty)
		val scalaScript = script.substring(start)
		scalaScript.trim() match {
			case "" => prev match {
				case Some(res) => res
				case None => InterpreterSuccess(None, "", None)
			}
			case _ => interpretScala(scalaScript, prev)
		}
	}
}